package  tools

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/sirupsen/logrus"
	"io"
	"sync"

	//"github.com/docker/docker/errdefs"
	"io/ioutil"
	//"net/http"
	"strconv"
	"strings"

	//	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
	//"io"
	//"os"
	"time"
	//	"os/exec"
	"context"
)

// 给 PullImage 函数创建一个互斥锁，防止并发的下载任务出现，可能会产生镜像别名的重名问题
type TagLock struct {
	sync.Mutex
}

// 生成全局互斥锁
var tl TagLock

// 下载镜像，传入镜像名，通过时间戳定义为镜像别名上传至 hub.docker.com
// 返回镜像别名，以及错误信息
func PullImage(ImageName string) (string, error){
	// 镜像的重名操作是一个互斥操作，并发发生可能发生不同镜像使用一个镜像别名的情况
	// 所以当存在镜像下载以及重名操作是，添加互斥锁
	tl.Lock()
	defer tl.Unlock()

	// 检测镜像是否曾经已经被缓存过，如果曾经缓存则报错
	returcode, err := CheckImagePull(ImageName)

	// 镜像是否缓存检测失败
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "PullImage",
		}).Warn(err)
	}

	// 检测镜像是否缓存，如果未缓存执行下载流程
	if returcode != 1 {
		ctx := context.Background()
		cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
		if err != nil {
			Log.WithFields(logrus.Fields{
				"msg": "PullImage",
			}).Warn(err)

			return "", nil
		}

		out, err := cli.ImagePull(ctx, ImageName, types.ImagePullOptions{})
		if err != nil {
			Log.WithFields(logrus.Fields{
				"msg": "PullImage",
			}).Warn(err)

			return "", nil
		}

		defer out.Close()

		//输出日志的地方
		//如果您不执行此步骤，则main将在ImagePull有机会在后台完成之前返回。
		//但是，如果您确实执行io.Copy，则复制会使main保持繁忙状态足够长的时间，以使提取操作实际完成。
		//在上面注释中的示例中，我让main忙于for/sleep循环。
		//看到所有输出的原因是……好吧，您将ImagePull的输出复制到Stdout到io.Copy(os.Stdout, out)，因此我们在stdout中看到ImagePull的输出也就不足为奇了。
		//相反，如果我们将所有输出复制到某个使输出消失的“黑洞”中，那么在复制发生时，我们将使main处于繁忙状态，同时抑制其所有输出。
		//一种方法是使用io/ioutil的ioutil.Discard。这是一个io.Writer，仅丢弃所有写入其中的数据。
		io.Copy(ioutil.Discard, out)
		Log.WithFields(logrus.Fields{
			"msg": "PullImage",
		}).Info(fmt.Sprintf("开始下载%v镜像", ImageName))

		// 生成镜像名,通过时间戳生成 image tag
		todayint, _ := time.ParseInLocation("2006-01-02 15:04:05" ,
			time.Now().Format("2006-01-02 15:04:05"), time.Local)
		imageAliasName := fmt.Sprintf("%v:%v", Cfg.Section("docker").Key("registory").String(),strconv.FormatInt((todayint.Unix()), 15))

		// fmt.Println("镜像的别名是：", imageAliasName)
		// 将镜像信息写入至数据库
		err = WriteImageDB(ImageName, imageAliasName)
		Log.WithFields(logrus.Fields{
			"msg": "PullImage",
		}).Info(fmt.Sprintf("开始将镜像与别名写入数据库：%v -- %v", ImageName, imageAliasName))
		return imageAliasName, nil
	} else {
		return "", nil
	}
}

// 定义推送镜像函数，指定推送的镜像名，返回 error 信息
func PushImage(image string) error {
	authConfig := types.AuthConfig{
		Username: Cfg.Section("docker").Key("username").String(),
		Password: Cfg.Section("docker").Key("password").String(),
	}

	encodedJSON, err := json.Marshal(authConfig)
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "PushImage",
		}).Warn(err)
		return err
	}
	authStr := base64.URLEncoding.EncodeToString(encodedJSON)
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "PushImage",
		}).Warn(err)
		panic(err)
	}

	out, err := cli.ImagePush(context.TODO(), image, types.ImagePushOptions{RegistryAuth: authStr})

	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "PushImage",
		}).Warn(err)
		return err
	}

	defer out.Close()

	body, err := ioutil.ReadAll(out)
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "PushImage",
		}).Warn(err)
		return err
	}

	if strings.Contains(string(body), "error") {
		return fmt.Errorf("push image to docker error")
	} else {
		return  nil
	}
}

// 镜像重命名函数，传入镜像原名以及改名后镜像名
// 返回 reTag 操作的 error
func ImageTag(sourceimagename string, renametag string) error{
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "ImageTag",
		}).Warn(err)
		return err
	}

	err = cli.ImageTag(ctx, sourceimagename, renametag)
	if err != nil {
		Log.WithFields(logrus.Fields{
			"msg": "ImageTag",
		}).Warn(err)
		return err
	}
	return  nil
}